#include "common.h"
#include "linux/sched.h"
#include "linux/kernel.h"
#include "asm/segment.h"

#include "fcntl.h"
#include "errno.h"
#include "const.h"
#include "sys/stat.h"

#define ACC_MODE(x) ("\004\002\006\377"[(x) & O_ACCMODE])

//#define NO_TRUNCATE

#define MAY_EXEC 1
#define MAY_WRITE 2
#define MAY_READ 4

static int permission(struct m_inode *inode,int mask)
{
    int mode = inode -> i_mode;

    //special case:not even root can read/write a deleted file
    if((inode -> i_dev) && (!inode -> i_nlinks))
    {
        return 0;
    }
    else if(current -> euid == inode -> i_uid)
    {
        mode >>= 6;
    }
    else if(current -> egid == inode -> i_gid)
    {
        mode >>= 3;
    }

    if(((mode & mask & 0007) == mask) || suser())
    {
        return 1;
    }

    return 0;
}

static int match(int len,const char *name,struct dir_entry *de)
{
    if((!de) || (!de -> inode) || (len > NAME_LEN))
    {
        return 0;
    }

    if((len < NAME_LEN) && (de -> name[len]))
    {
        return 0;
    }

    for(ulong i = 0;i < len;i++)
    {
        if(get_fs_byte(name + i) != de -> name[i])
        {
            return 0;
        }
    }

    return 1;
}

static struct buffer_head *find_entry(struct m_inode **dir,const char *name,int namelen,struct dir_entry **res_dir)
{
    int entries;
    int block,i;
    struct buffer_head *bh;
    struct dir_entry *de;
    struct super_block *sb;

    #ifdef NO_TRUNCATE
        if(namelen > NAME_LEN)
        {
            return NULL;
        }

    #else
        if(namelen > NAME_LEN)
        {
            namelen = NAME_LEN;
        }

    #endif

    entries = (*dir) -> i_size / (sizeof(struct dir_entry));
    *res_dir = NULL;

    if(!namelen)
    {
        return NULL;
    }

    //check for '..',as we might have to do some "magic" for it
    if((namelen == 2) && (get_fs_byte((const uint8_t *)name) == '.') && (get_fs_byte(((const uint8_t *)name) + 1) == '.'))
    {
        //'..' in a pseudo-root results in a faked '.'(just change namelen)
        if((*dir) == current -> root)
        {
            namelen = 1;
        }
        else if((*dir) -> i_num == ROOT_INO)
        {
            sb = get_super((*dir) -> i_dev);

            if(sb -> s_imount)
            {
                iput(*dir);
                (*dir) = sb -> s_imount;
                (*dir) -> i_count++;
            }
        }
    }

    if(!(block = (*dir) -> i_zone[0]))
    {
        return NULL;
    }

    if(!(bh = bread((*dir) -> i_dev,block)))
    {
        return NULL;
    }

    i = 0;
    de = (struct dir_entry *)bh -> b_data;

    while(i < entries)
    {
        if(((char *)de) > (BLOCK_SIZE + bh -> b_data))
        {
            brelse(bh);
            bh = NULL;

            if(!(block = bmap(*dir,i / DIR_ENTRIES_PER_BLOCK)) || (!(bh = bread((*dir) -> i_dev,block))))
            {
                i += DIR_ENTRIES_PER_BLOCK;
                continue;
            }

            de = (struct dir_entry *)bh -> b_data;
        }
        de -> name;
        if(match(namelen,name,de))
        {
            *res_dir = de;
            return bh;
        }

        de++;
        i++;
    }

    brelse(bh);
    return NULL;
}

static struct buffer_head *add_entry(struct m_inode *dir,const char *name,int namelen,struct dir_entry **res_dir)
{
    int block,i;
    struct buffer_head *bh;
    struct dir_entry *de;

    *res_dir = NULL;

    #ifdef NO_TRUNCATE
        if(namelen > NAME_LEN)
        {
            return NULL;
        }
    #else
        if(namelen > NAME_LEN)
        {
            namelen = NAME_LEN;
        }
    #endif

    if(!namelen)
    {
        return NULL;
    }

    if(!(block = dir -> i_zone[0]))
    {
        return NULL;
    }

    if(!(bh = bread(dir -> i_dev,block)))
    {
        return NULL;
    }

    i = 0;
    de = (struct dir_entry *)bh -> b_data;

    while(1)
    {
        if(((char *)de) >= (BLOCK_SIZE + bh -> b_data))
        {
            brelse(bh);
            bh = NULL;
            block = create_block(dir,i / DIR_ENTRIES_PER_BLOCK);

            if(!block)
            {
                return NULL;
            }

            if(!(bh = bread(dir -> i_dev,block)))
            {
                i += DIR_ENTRIES_PER_BLOCK;
                continue;
            }

            de = (struct dir_entry *)bh -> b_data;
        }

        if(i * sizeof(struct dir_entry) >= dir -> i_size)
        {
            de -> inode = 0;
            dir -> i_size = (i + 1) * sizeof(struct dir_entry);
            dir -> i_dirt = 1;
            dir -> i_ctime = CURRENT_TIME;
        }

        if(!de -> inode)
        {
            dir -> i_mtime = CURRENT_TIME;

            for(i = 0;i < NAME_LEN;i++)
            {
                de -> name[i] = (i < namelen) ? get_fs_byte((const uint8_t *)name + i) : 0;
            }

            bh -> b_dirt = 1;
            *res_dir = de;
            return bh;
        }

        de++;
        i++;
    }

    brelse(bh);
    return NULL;
}

static struct m_inode *get_dir(const char *pathname)
{
    char c;
    const char *thisname;
    struct m_inode *inode;
    struct buffer_head *bh;
    int namelen,inr,idev;
    struct dir_entry *de;

    if((!current -> root) || (!current -> root -> i_count))
    {
        panic("No root inode");
    }

    if((!current -> pwd) || (!current -> pwd -> i_count))
    {
        panic("No cwd inode");
    }

    if((c = get_fs_byte((const uint8_t *)pathname)) == '/')
    {
        inode = current -> root;
        pathname++;
    }
    else if(c)
    {
        inode = current -> pwd;
    }
    else
    {
        return NULL;
    }

    inode -> i_count++;

    while(1)
    {
        thisname = pathname;

        if(!S_ISDIR(inode -> i_mode) || (!permission(inode,MAY_EXEC)))
        {
            iput(inode);
            return NULL;
        }
        for(namelen = 0;(c = get_fs_byte((const uint8_t *)(pathname++))) && (c != '/');namelen++)
        {
            //nothing    
        }

        if(!c)
        {
            return inode;
        }
        
        if(!(bh = find_entry(&inode,thisname,namelen,&de)))
        {
            iput(inode);
            return NULL;
        }
        
        inr = de -> inode;
        idev = inode -> i_dev;
        brelse(bh);
        iput(inode);

        if(!(inode = iget(idev,inr)))
        {
            return NULL;
        }
    }
}

static struct m_inode *dir_namei(const char *pathname,int *namelen,const char **name)
{
    char c;
    const char *basename;
    struct m_inode *dir;
    
    if(!(dir = get_dir(pathname)))
    {
        return NULL;
    }
    
    basename = pathname;

    while(c = get_fs_byte((const uint8_t *)(pathname++)))
    {
        if(c == '/')
        {
            basename = pathname;
        }
    }

    *namelen = pathname - basename - 1;
    *name = basename;
    return dir;
}

struct m_inode *namei(const char *pathname)
{
    const char *basename;
    int inr,dev,namelen;
    struct m_inode *dir;
    struct buffer_head *bh;
    struct dir_entry *de;
    
    if(!(dir = dir_namei(pathname,&namelen,&basename)))
    {
        return NULL;
    }
    
    if(!namelen)
    {
        return dir;
    }

    bh = find_entry(&dir,basename,namelen,&de);
    
    if(!bh)
    {
        iput(dir);
        return NULL;
    }

    inr = de -> inode;
    dev = dir -> i_dev;
    brelse(bh);
    iput(dir);
    dir = iget(dev,inr);

    if(dir)
    {
        dir -> i_atime = CURRENT_TIME;
        dir -> i_dirt = 1;
    }

    return dir;
}

int open_namei(const char *pathname,int flag,int mode,struct m_inode **res_inode)
{
    const char *basename;
    int inr,dev,namelen;
    struct m_inode *dir,*inode;
    struct buffer_head *bh;
    struct dir_entry *de;

    if((flag & O_TRUNC) && (!(flag & O_ACCMODE)))
    {
        flag |= O_WRONLY;
    }

    mode &= 0777 & (~current -> umask);
    mode |= I_REGULAR;

    if(!(dir = dir_namei(pathname,&namelen,&basename)))
    {
        return -ENOENT;
    }

    if(!namelen)
    {
        if(!(flag & (O_ACCMODE | O_CREAT | O_TRUNC)))
        {
            *res_inode = dir;
            return 0;
        }

        iput(dir);
        return -EISDIR;
    }

    bh = find_entry(&dir,basename,namelen,&de);
    
    if(!bh)
    {
        if(!(flag & O_CREAT))
        {
            iput(dir);
            return -ENOENT;
        }

        if(!permission(dir,MAY_WRITE))
        {
            iput(dir);
            return -EACCES;
        }

        inode = new_inode(dir -> i_dev);

        if(!inode)
        {
            iput(dir);
            return -ENOSPC;
        }

        inode -> i_uid = current -> euid;
        inode -> i_mode = mode;
        inode -> i_dirt = 1;
        bh = add_entry(dir,basename,namelen,&de);

        if(!bh)
        {
            inode -> i_nlinks--;
            iput(inode);
            iput(dir);
            return -ENOSPC;
        }

        de -> inode = inode -> i_num;
        bh -> b_dirt = 1;
        brelse(bh);
        iput(dir);
        *res_inode = inode;
        return 0;
    }

    inr = de -> inode;
    dev = dir -> i_dev;
    brelse(bh);
    iput(dir);

    if(flag & O_EXCL)
    {
        return -EEXIST;
    }

    if(!(inode = iget(dev,inr)))
    {
        return -EACCES;
    }

    if((S_ISDIR(inode -> i_mode) && (flag & O_ACCMODE)) || (!permission(inode,ACC_MODE(flag))))
    {
        iput(inode);
        return -EPERM;
    }

    inode -> i_atime = CURRENT_TIME;

    if(flag & O_TRUNC)
    {
        truncate(inode);
    }

    *res_inode = inode;
    return 0;
}

int64_t sys_mknod(const char *filename,int mode,int dev)
{
    const char *basename;
    int namelen;
    struct m_inode *dir,*inode;
    struct buffer_head *bh;
    struct dir_entry *de;

    if(!suser())
    {
        return -EPERM;
    }

    if(!(dir = dir_namei(filename,&namelen,&basename)))
    {
        return -ENOENT;
    }

    if(!namelen)
    {
        iput(dir);
        return -ENOENT;
    }

    if(!permission(dir,MAY_WRITE))
    {
        iput(dir);
        return -EPERM;
    }

    bh = find_entry(&dir,basename,namelen,&de);

    if(bh)
    {
        brelse(bh);
        iput(dir);
        return -EEXIST;
    }

    inode = new_inode(dir -> i_dev);

    if(!inode)
    {
        iput(dir);
        return -ENOSPC;
    }

    inode -> i_mode = mode;

    if(S_ISBLK(mode) || S_ISCHR(mode))
    {
        inode -> i_zone[0] = dev;
    }

    inode -> i_mtime = inode -> i_atime = CURRENT_TIME;
    inode -> i_dirt = 1;
    bh = add_entry(dir,basename,namelen,&de);

    if(!bh)
    {
        iput(dir);
        inode -> i_nlinks = 0;
        iput(inode);
        return -ENOSPC;
    }

    de -> inode = inode -> i_num;
    bh -> b_dirt = 1;
    iput(dir);
    iput(inode);
    brelse(bh);
    return 0;
}

int64_t sys_mkdir(const char *pathname,int mode)
{
    const char *basename;
    int namelen;
    struct m_inode *dir,*inode;
    struct buffer_head *bh,*dir_block;
    struct dir_entry *de;

    if(!suser())
    {
        return -EPERM;
    }

    if(!(dir = dir_namei(pathname,&namelen,&basename)))
    {
        return -ENOENT;
    }

    if(!namelen)
    {
        iput(dir);
        return -ENOENT;
    }

    if(!permission(dir,MAY_WRITE))
    {
        iput(dir);
        return -EPERM;
    }

    bh = find_entry(&dir,basename,namelen,&de);

    if(bh)
    {
        brelse(bh);
        iput(dir);
        return -EEXIST;
    }

    inode = new_inode(dir -> i_dev);

    if(!inode)
    {
        iput(dir);
        return -ENOSPC;
    }

    inode -> i_size = 32;
    inode -> i_dirt = 1;
    inode -> i_mtime = inode -> i_atime = CURRENT_TIME;

    if(!(inode -> i_zone[0] = new_block(inode -> i_dev)))
    {
        iput(dir);
        inode -> i_nlinks--;
        iput(inode);
        return -ENOSPC;
    }

    inode -> i_dirt = 1;

    if(!(dir_block = bread(inode -> i_dev,inode -> i_zone[0])))
    {
        iput(dir);
        free_block(inode -> i_dev,inode -> i_zone[0]);
        inode -> i_nlinks--;
        iput(inode);
        return -ERROR;
    }

    de = (struct dir_entry *)dir_block -> b_data;
    de -> inode = inode -> i_num;
    strcpy(de -> name,".");
    de++;
    de -> inode = dir -> i_num;
    strcpy(de -> name,"..");
    inode -> i_nlinks = 2;
    dir_block -> b_dirt = 1;
    brelse(dir_block);
    inode -> i_mode = I_DIRECTORY | (mode & 0777 & (~current -> umask));
    inode -> i_dirt = 1;
    bh = add_entry(dir,basename,namelen,&de);

    if(!bh)
    {
        iput(dir);
        free_block(inode -> i_dev,inode -> i_zone[0]);
        inode -> i_nlinks = 0;
        iput(inode);
        return -ENOSPC;
    }

    de -> inode = inode -> i_num;
    bh -> b_dirt = 1;
    dir -> i_nlinks++;
    dir -> i_dirt = 1;
    iput(dir);
    iput(inode);
    brelse(bh);
    return 0;
}

static int empty_dir(struct m_inode *inode)
{
    int nr,block;
    int len;
    struct buffer_head *bh;
    struct dir_entry *de;

    len = inode -> i_size / sizeof(struct dir_entry);
    
    if((len < 2) || (!inode -> i_zone[0]) || (!(bh = bread(inode -> i_dev,inode -> i_zone[0]))))
    {
        printk("warning - bad directory on dev %04x\r\n",inode -> i_dev);
        return 0;
    }

    de = (struct dir_entry *)bh -> b_data;

    if((de[0].inode != inode -> i_num) || (!de[1].inode) || strcmp(".",de[0].name) || strcmp("..",de[1].name))
    {
        printk("warning - bad directory on dev %04x\r\n",inode -> i_dev);
        return 0;
    }

    nr = 2;
    de += 2;
    
    while(nr < len)
    {
        if(((void *)de) >= ((void *)(bh -> b_data + BLOCK_SIZE)))
        {
            brelse(bh);
            block = bmap(inode,nr / DIR_ENTRIES_PER_BLOCK);

            if(!block)
            {
                nr += DIR_ENTRIES_PER_BLOCK;
                continue;
            }

            if(!(bh = bread(inode -> i_dev,block)))
            {
                return 0;
            }

            de = (struct dir_entry *)bh -> b_data;
        }

        if(de -> inode)
        {
            brelse(bh);
            return 0;
        }

        de++;
        nr++;
    }

    brelse(bh);
    return 1;
}

int64_t sys_rmdir(const char *name)
{
    const char *basename;
    int namelen;
    struct m_inode *dir,*inode;
    struct buffer_head *bh;
    struct dir_entry *de;

    if(!suser())
    {
        return -EPERM;
    }

    if(!(dir = dir_namei(name,&namelen,&basename)))
    {
        return -ENOENT;
    }

    if(!namelen)
    {
        iput(dir);
        return -ENOENT;
    }

    if(!permission(dir,MAY_WRITE))
    {
        iput(dir);
        return -EPERM;
    }

    bh = find_entry(&dir,basename,namelen,&de);

    if(!bh)
    {
        iput(dir);
        return -ENOENT;
    }

    if(!(inode = iget(dir -> i_dev,de -> inode)))
    {
        iput(dir);
        brelse(bh);
        return -EPERM;
    }

    if((dir -> i_mode & S_ISVTX) && current -> euid && (inode -> i_uid != current -> euid))
    {
        iput(dir);
        iput(inode);
        brelse(bh);
        return -EPERM;
    }

    if((inode -> i_dev != dir -> i_dev) || (inode -> i_count > 1))
    {
        iput(dir);
        iput(inode);
        brelse(bh);
        return -EPERM;
    }

    if(inode == dir)
    {
        iput(inode);
        iput(dir);
        brelse(bh);
        return -EPERM;
    }

    if(!S_ISDIR(inode -> i_mode))
    {
        iput(inode);
        iput(dir);
        brelse(bh);
        return -ENOTDIR;
    }

    if(!empty_dir(inode))
    {
        iput(inode);
        iput(dir);
        brelse(bh);
        return -ENOTEMPTY;
    }

    if(inode -> i_nlinks != 2)
    {
        printk("empty directory has nlink != 2 (%d)",inode -> i_nlinks);
    }

    de -> inode = 0;
    bh -> b_dirt = 1;
    brelse(bh);
    inode -> i_nlinks = 0;
    inode -> i_dirt = 1;
    dir -> i_nlinks--;
    dir -> i_ctime = dir -> i_mtime = CURRENT_TIME;
    dir -> i_dirt = 1;
    iput(dir);
    iput(inode);
    return 0;
}

int64_t sys_unlink(const char *name)
{
    const char *basename;
    int namelen;
    struct m_inode *dir,*inode;
    struct buffer_head *bh;
    struct dir_entry *de;

    if(!(dir = dir_namei(name,&namelen,&basename)))
    {
        return -ENOENT;
    }

    if(!namelen)
    {
        iput(dir);
        return -ENOENT;
    }

    if(!permission(dir,MAY_WRITE))
    {
        iput(dir);
        return -EPERM;
    }

    bh = find_entry(&dir,basename,namelen,&de);

    if(!bh)
    {
        iput(dir);
        return -ENOENT;
    }

    if(!(inode = iget(dir -> i_dev,de -> inode)))
    {
        iput(dir);
        brelse(bh);
        return -ENOENT;
    }

    if((dir -> i_mode & S_ISVTX) && (!suser()) && (current -> euid != inode -> i_uid) && (current -> euid != dir -> i_uid))
    {
        iput(dir);
        iput(inode);
        brelse(bh);
        return -EPERM;
    }

    if(S_ISDIR(inode -> i_mode))
    {
        iput(inode);
        iput(dir);
        brelse(bh);
        return -EPERM;
    }

    if(!inode -> i_nlinks)
    {
        printk("Deleteing nonexistent file (%04x:%d), %d\r\n",inode -> i_dev,inode -> i_num,inode -> i_nlinks);
        inode -> i_nlinks = 1;
    }

    de -> inode = 0;
    bh -> b_dirt = 1;
    brelse(bh);
    inode -> i_nlinks--;
    inode -> i_dirt = 1;
    inode -> i_ctime = CURRENT_TIME;
    iput(inode);
    iput(dir);
    return 0;
}

int64_t sys_link(const char *oldname,const char *newname)
{
    struct dir_entry *de;
    struct m_inode *oldinode,*dir;
    struct buffer_head *bh;
    const char *basename;
    int namelen;

    oldinode = namei(oldname);

    if(!oldinode)
    {
        return -ENOENT;
    }

    if(S_ISDIR(oldinode -> i_mode))
    {
        iput(oldinode);
        return -EPERM;
    }

    dir = dir_namei(newname,&namelen,&basename);

    if(!dir)
    {
        iput(oldinode);
        return -EACCES;
    }

    if(!namelen)
    {
        iput(oldinode);
        iput(dir);
        return -EPERM;
    }

    if(dir -> i_dev != oldinode -> i_dev)
    {
        iput(dir);
        iput(oldinode);
        return -EXDEV;
    }

    if(!permission(dir,MAY_WRITE))
    {
        iput(dir);
        iput(oldinode);
        return -EACCES;
    }

    bh = find_entry(&dir,basename,namelen,&de);

    if(bh)
    {
        brelse(bh);
        iput(dir);
        iput(oldinode);
        return -EEXIST;
    }

    bh = add_entry(dir,basename,namelen,&de);

    if(!bh)
    {
        iput(dir);
        iput(oldinode);
        return -ENOSPC;
    }

    de -> inode = oldinode -> i_num;
    bh -> b_dirt = 1;
    brelse(bh);
    iput(dir);
    oldinode -> i_nlinks++;
    oldinode -> i_ctime = CURRENT_TIME;
    oldinode -> i_dirt = 1;
    iput(oldinode);
    return 0;
}